// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "services/audio/output_device_mixer_manager.h"

#include "base/check.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/trace_event/trace_event.h"
#include "media/audio/audio_io.h"
#include "media/audio/audio_manager.h"
#include "media/base/audio_latency.h"
#include "services/audio/audio_manager_power_user.h"
#include "services/audio/device_listener_output_stream.h"
#include "services/audio/output_device_mixer.h"

namespace {

const char kNormalizedDefaultDeviceId[] = "";

// Helper function which returns a consistent representation of the default
// device ID.
std::string NormalizeIfDefault(const std::string& device_id) {
  return media::AudioDeviceDescription::IsDefaultDevice(device_id)
             ? kNormalizedDefaultDeviceId
             : device_id;
}

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// Aligned with AudioOutputDeviceMixerManagerStreamCreation enum.
enum class StreamCreation {
  kUnmixable,
  kFallbackToUnmixable,
  kUsingNewMixer,
  kUsingExistingMixer,
  kMaxValue = kUsingExistingMixer,
};

}  // namespace

namespace audio {

OutputDeviceMixerManager::OutputDeviceMixerManager(
    media::AudioManager* audio_manager,
    OutputDeviceMixer::CreateCallback create_mixer_callback)
    : audio_manager_(audio_manager),
      current_default_device_id_(
          AudioManagerPowerUser(audio_manager_).GetDefaultOutputDeviceID()),
      current_communication_device_id_(AudioManagerPowerUser(audio_manager_)
                                           .GetCommunicationsOutputDeviceID()),
      create_mixer_callback_(std::move(create_mixer_callback)),
      device_change_weak_ptr_factory_(this) {
  // This should be a static_assert, but there is no compile time way to run
  // AudioDeviceDescription::IsDefaultDevice().
  DCHECK(media::AudioDeviceDescription::IsDefaultDevice(
      kNormalizedDefaultDeviceId));
}

OutputDeviceMixerManager::~OutputDeviceMixerManager() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
}

media::AudioOutputStream* OutputDeviceMixerManager::MakeOutputStream(
    const std::string& device_id,
    const media::AudioParameters& params,
    base::OnceClosure close_stream_on_device_change) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  OutputDeviceMixer* mixer = nullptr;
  StreamCreation stream_creation = StreamCreation::kUnmixable;

  if (params.format() == media::AudioParameters::AUDIO_PCM_LOW_LATENCY) {
    std::string mixer_device_id = ToMixerDeviceId(device_id);
    mixer = FindMixer(mixer_device_id);
    if (mixer) {
      stream_creation = StreamCreation::kUsingExistingMixer;
    } else {
      mixer = AddMixer(mixer_device_id);
      stream_creation = mixer ? StreamCreation::kUsingNewMixer
                              : StreamCreation::kFallbackToUnmixable;
    }
  }

  base::UmaHistogramEnumeration(
      "Media.Audio.OutputDeviceMixerManager.StreamCreation", stream_creation);

  if (mixer) {
    return mixer->MakeMixableStream(params,
                                    std::move(close_stream_on_device_change));
  }

  DLOG(WARNING) << "Making unmixable output stream";
  return CreateDeviceListenerStream(std::move(close_stream_on_device_change),
                                    device_id, params);
}

void OutputDeviceMixerManager::OnDeviceChange() {
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("audio"),
               "OutputDeviceMixerManager::OnDeviceChange");
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  current_default_device_id_ =
      AudioManagerPowerUser(audio_manager_).GetDefaultOutputDeviceID();

  current_communication_device_id_ =
      AudioManagerPowerUser(audio_manager_).GetCommunicationsOutputDeviceID();

  // Invalidate WeakPtrs, cancelling any pending device change callbacks
  // generated by the same device change event.
  device_change_weak_ptr_factory_.InvalidateWeakPtrs();

  OutputDeviceMixers old_mixers;
  output_device_mixers_.swap(old_mixers);

  // Do not call StopListening(), as |old_mixers| are being destroyed anyways.
  for (auto&& mixer : old_mixers)
    mixer->ProcessDeviceChange();
}

void OutputDeviceMixerManager::StartNewListener(
    ReferenceOutput::Listener* listener,
    const std::string& listener_device_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  DCHECK(IsNormalizedIfDefault(listener_device_id));

  DCHECK(!listener_registration_.contains(listener));
  listener_registration_[listener] = listener_device_id;

  OutputDeviceMixer* mixer = FindMixer(ToMixerDeviceId(listener_device_id));

  if (!mixer)
    return;

  mixer->StartListening(listener);
}

void OutputDeviceMixerManager::StartListening(
    ReferenceOutput::Listener* listener,
    const std::string& output_device_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  std::string listener_device_id = NormalizeIfDefault(output_device_id);

  if (!listener_registration_.contains(listener)) {
    StartNewListener(listener, listener_device_id);
    return;
  }

  std::string registered_listener_device_id =
      listener_registration_.at(listener);

  if (ToMixerDeviceId(registered_listener_device_id) !=
      ToMixerDeviceId(listener_device_id)) {
    // |listener| is listening to a completely different mixer.
    StopListening(listener);
    StartNewListener(listener, listener_device_id);
    return;
  }

  // |listener| is listening to the right mixer, but we might need to update
  // its registration (e.g. when switching between
  // |current_default_device_id_| and kNormalizedDefaultId, or
  // |current_communications_device_id_| and kCommunicationsDeviceId).
  if (registered_listener_device_id != listener_device_id)
    listener_registration_[listener] = listener_device_id;
}

void OutputDeviceMixerManager::StopListening(
    ReferenceOutput::Listener* listener) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  const std::string listener_device_id = listener_registration_.at(listener);
  listener_registration_.erase(listener);

  OutputDeviceMixer* mixer = FindMixer(ToMixerDeviceId(listener_device_id));

  // The mixer was never created, because there was no playback to that device
  // (possibly after a device device change). Listening never started, so there
  // is nothing to stop.
  if (!mixer)
    return;

  mixer->StopListening(listener);
}

std::string OutputDeviceMixerManager::ToMixerDeviceId(
    const std::string& device_id) {
  if (media::AudioDeviceDescription::IsDefaultDevice(device_id))
    return kNormalizedDefaultDeviceId;

  DCHECK(!device_id.empty());

  if (device_id == current_default_device_id_)
    return kNormalizedDefaultDeviceId;

  // It's possible for |current_communication_device_id_| and
  // |current_default_device_id_| to match. In that case, replace the
  // communications mixer device ID with the default mixer device ID.
  // Similarly, replace "communications" with kNormalizedDefaultDeviceId when
  // |current_communication_device_id_| is unsupported/unconfigured.
  if (device_id == media::AudioDeviceDescription::kCommunicationsDeviceId &&
      (current_communication_device_id_.empty() ||
       current_communication_device_id_ == current_default_device_id_)) {
    return kNormalizedDefaultDeviceId;
  }

  if (device_id == current_communication_device_id_)
    return media::AudioDeviceDescription::kCommunicationsDeviceId;

  return device_id;
}

OutputDeviceMixer* OutputDeviceMixerManager::FindMixer(
    const std::string& device_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  DCHECK_EQ(ToMixerDeviceId(device_id), device_id);

  for (const auto& mixer : output_device_mixers_) {
    if (mixer->device_id() == device_id)
      return mixer.get();
  }

  return nullptr;
}

OutputDeviceMixer* OutputDeviceMixerManager::AddMixer(
    const std::string& device_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  DCHECK_EQ(ToMixerDeviceId(device_id), device_id);

  DCHECK(!FindMixer(device_id));

  media::AudioParameters output_params =
      AudioManagerPowerUser(audio_manager_)
          .GetOutputStreamParameters(device_id);

  if (!output_params.IsValid()) {
    LOG(ERROR) << "Adding OutputDeviceMixer failed: invalid output parameters";
    return nullptr;
  }

  output_params.set_frames_per_buffer(media::AudioLatency::GetRtcBufferSize(
      output_params.sample_rate(), output_params.frames_per_buffer()));

  // TODO(crbug/1295658): Temporary work around. Mix all audio as stereo and
  // rely on the system channel mapping.
  if (output_params.channel_layout() == media::CHANNEL_LAYOUT_DISCRETE &&
      output_params.channels() >= 2) {
    output_params.Reset(
        output_params.format(), media::ChannelLayoutConfig::Stereo(),
        output_params.sample_rate(), output_params.frames_per_buffer());
  }

  // base::Unretained(this) is safe here, because |output_device_mixers_|
  // are owned by |this|.
  std::unique_ptr<OutputDeviceMixer> output_device_mixer =
      create_mixer_callback_.Run(
          device_id, output_params,
          base::BindRepeating(&OutputDeviceMixerManager::CreateMixerOwnedStream,
                              base::Unretained(this)),
          audio_manager_->GetTaskRunner());

  // The |device_id| might no longer be valid, e.g. if a device was unplugged.
  if (!output_device_mixer) {
    LOG(ERROR) << "Adding OutputDeviceMixer failed: creation error";
    return nullptr;
  }

  auto* mixer = output_device_mixer.get();
  output_device_mixers_.push_back(std::move(output_device_mixer));

  // Attach any interested listeners.
  for (auto&& listener_device_kvp : listener_registration_) {
    if (ToMixerDeviceId(listener_device_kvp.second) == mixer->device_id())
      mixer->StartListening(listener_device_kvp.first);
  }

  return mixer;
}

base::OnceClosure OutputDeviceMixerManager::GetOnDeviceChangeCallback() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  return base::BindOnce(&OutputDeviceMixerManager::OnDeviceChange,
                        device_change_weak_ptr_factory_.GetWeakPtr());
}

media::AudioOutputStream* OutputDeviceMixerManager::CreateMixerOwnedStream(
    const std::string& device_id,
    const media::AudioParameters& params) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
  return CreateDeviceListenerStream(GetOnDeviceChangeCallback(), device_id,
                                    params);
}

media::AudioOutputStream* OutputDeviceMixerManager::CreateDeviceListenerStream(
    base::OnceClosure on_device_change_callback,
    const std::string& device_id,
    const media::AudioParameters& params) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);

  media::AudioOutputStream* stream =
      audio_manager_->MakeAudioOutputStreamProxy(params, device_id);
  if (!stream) {
    LOG(ERROR) << "Stream proxy limit reached";
    return nullptr;
  }

  // If this stream is created via CreateMixerOwnedStream(),
  // |on_device_change_callback| will call OnDeviceChange(), cancel pending
  // calls to OnDeviceChange(), and release all mixer owned streams.
  //
  // If we are directly creating this stream, |on_device_change_callback| will
  // synchronously close the returned stream.
  return new DeviceListenerOutputStream(audio_manager_, stream,
                                        std::move(on_device_change_callback));
}

bool OutputDeviceMixerManager::IsNormalizedIfDefault(
    const std::string& device_id) {
  return device_id == kNormalizedDefaultDeviceId ||
         !media::AudioDeviceDescription::IsDefaultDevice(device_id);
}

}  // namespace audio
